using System;
using System.Collections.Generic;
using System.Text;

namespace Yorrick.FlickrMetadataSynchr.Data
{
   /// <summary>
   /// Class that holds the metadata of a picture that is to be synched and an identifier.
   /// </summary>
   /// <remarks>This class acts as a base class for both local and Flickr metadata.
   /// This class implements change tracking on a property-by-property basis. It does this by
   /// remembering the original values. The original values can be changed by setting 
   /// the regular properties until change tracking is turned on.</remarks>
   public abstract class PictureMetadata
   {
      #region Private constants
      /// <summary>
      /// The string comparer to use for tags.
      /// </summary>
      private static readonly StringComparer _tagComparer = StringComparer.CurrentCulture;
      #endregion

      #region Public static fields
      /// <summary>
      /// An empty tag collection.
      /// </summary>
      public static readonly string[] NoTags = new string[0];
      #endregion

      #region Private fields
      /// <summary>
      /// The id of the picture.
      /// </summary>
      private string _id;

      /// <summary>
      /// The GPS latitude.
      /// </summary>
      private double? _gpsLatitude = null;

      /// <summary>
      /// The original GPS latitude.
      /// </summary>
      private double? _gpsLatitudeOriginal = null;

      /// <summary>
      /// The GPS longitude.
      /// </summary>
      private double? _gpsLongitude = null;

      /// <summary>
      /// The original GPS longitude.
      /// </summary>
      private double? _gpsLongitudeOriginal = null;

      /// <summary>
      /// The date and time at which the picture was taken.
      /// </summary>
      private DateTime _dateTaken = DateTime.MinValue;

      /// <summary>
      /// The original date and time at which the picture was taken.
      /// </summary>
      private DateTime _dateTakenOriginal = DateTime.MinValue;

      /// <summary>
      /// The date and time at which the picture or its metadata was last updated.
      /// </summary>
      private DateTime _lastUpdated = DateTime.MinValue;

      /// <summary>
      /// The original date and time at which the picture or its metadata was last updated.
      /// </summary>
      private DateTime _lastUpdatedOriginal = DateTime.MinValue;

      /// <summary>
      /// The set of tags (keywords).
      /// </summary>
      private string[] _tags = NoTags;

      /// <summary>
      /// The original set of tags (keywords).
      /// </summary>
      private string[] _tagsOriginal = NoTags;

      /// <summary>
      /// The title.
      /// </summary>
      private string _title = String.Empty;

      /// <summary>
      /// The original title.
      /// </summary>
      private string _titleOriginal = String.Empty;

      /// <summary>
      /// The description.
      /// </summary>
      private string _description = String.Empty;

      /// <summary>
      /// The original description.
      /// </summary>
      private string _descriptionOriginal = String.Empty;

      /// <summary>
      /// The author.
      /// </summary>
      private string _author = String.Empty;

      /// <summary>
      /// The original author.
      /// </summary>
      private string _authorOriginal = String.Empty;

      /// <summary>
      /// Whether or not the data has changed.
      /// </summary>
      private bool _changed = false;

      /// <summary>
      /// Whether or not changes should be tracked.
      /// </summary>
      private bool _trackChanges = false;
      #endregion

      #region Constructor
      /// <summary>
      /// Constructs an instance with the specified id.
      /// </summary>
      /// <param name="id">The picture id.</param>
      public PictureMetadata(string id)
      {
         if (String.IsNullOrEmpty(id))
         {
            throw new ArgumentException("The id should be a non-empty string.", "id");
         }

         _id = id;
      }
      #endregion

      #region Public properties
      /// <summary>
      /// Gets the id of the picture.
      /// </summary>
      public string Id
      {
         get
         {
            return _id;
         }
      }

      /// <summary>
      /// Gets or set the GPS latitude
      /// </summary>
      public double? GpsLatitude
      {
         get
         {
            return _gpsLatitude;
         }
         set
         {
            if (_gpsLatitude != value)
            {
               _gpsLatitude = value;
               if (!_trackChanges)
               {
                  _gpsLatitudeOriginal = value;
               }
               else
               {
                  _changed = true;
               }
            }
         }
      }

      /// <summary>
      /// Gets whether or not the GPS latitude has changed.
      /// </summary>
      public bool GpsLatitudeChanged
      {
         get
         {
            return (_gpsLatitude != _gpsLatitudeOriginal);
         }
      }

      /// <summary>
      /// Gets the original GPS latitude
      /// </summary>
      public double? GpsLatitudeOriginal
      {
         get
         {
            return _gpsLatitudeOriginal;
         }
      }

      /// <summary>
      /// Gets or set the GPS longitude
      /// </summary>
      public double? GpsLongitude
      {
         get
         {
            return _gpsLongitude;
         }
         set
         {
            if (_gpsLongitude != value)
            {
               _gpsLongitude = value;
               if (!_trackChanges)
               {
                  _gpsLongitudeOriginal = value;
               }
               else
               {
                  _changed = true;
               }
            }
         }
      }

      /// <summary>
      /// Gets whether or not the GPS longitude has changed.
      /// </summary>
      public bool GpsLongitudeChanged
      {
         get
         {
            return (_gpsLongitude != _gpsLongitudeOriginal);
         }
      }

      /// <summary>
      /// Gets the original GPS longitude
      /// </summary>
      public double? GpsLongitudeOriginal
      {
         get
         {
            return _gpsLongitudeOriginal;
         }
      }

      /// <summary>
      /// The date and time at which the picture or its metadata was last updated.
      /// </summary>
      public DateTime LastUpdated
      {
         get
         {
            return _lastUpdated;
         }
         set
         {
            if (_lastUpdated != value)
            {
               _lastUpdated = value;
               if (!_trackChanges)
               {
                  _lastUpdatedOriginal = value;
               }
               else
               {
                  _changed = true;
               }
            }
         }
      }

      /// <summary>
      /// Gets whether or not date and time at which the picture or its metadata was last updated has changed.
      /// </summary>
      public bool LastUpdatedChanged
      {
         get
         {
            return (_lastUpdated != _lastUpdatedOriginal);
         }
      }

      /// <summary>
      /// Gets the original date and time at which the picture or its metadata was last updated.
      /// </summary>
      public DateTime LastUpdatedOriginal
      {
         get
         {
            return _lastUpdatedOriginal;
         }
      }

      /// <summary>
      /// Gets or set the date and time at which the picture was taken.
      /// </summary>
      public DateTime DateTaken
      {
         get
         {
            return _dateTaken;
         }
         set
         {
            if (_dateTaken != value)
            {
               _dateTaken = value;
               if (!_trackChanges)
               {
                  _dateTakenOriginal = value;
               }
               else
               {
                  _changed = true;
               }
            }
         }
      }

      /// <summary>
      /// Gets whether or not the date and time at which the picture was taken has changed.
      /// </summary>
      public bool DateTakenChanged
      {
         get
         {
            return (_dateTaken != _dateTakenOriginal);
         }
      }

      /// <summary>
      /// Gets the original date and time at which the picture was taken.
      /// </summary>
      public DateTime DateTakenOriginal
      {
         get
         {
            return _dateTakenOriginal;
         }
      }

      /// <summary>
      /// Gets or set the set of tags (keywords).
      /// </summary>
      public string[] Tags
      {
         get
         {
            return _tags;
         }
         set
         {
            if (value == null)
            {
               throw new ArgumentNullException();
            }
            if (!AreTagsEqual(_tags, value, _tagComparer))
            {
               _tags = value;
               if (!_trackChanges)
               {
                  _tagsOriginal = (string[]) _tags.Clone();
               }
               else
               {
                  _changed = true;
               }
            }
         }
      }

      /// <summary>
      /// Gets whether or not the tags have changed.
      /// </summary>
      public bool TagsChanged
      {
         get
         {
            return (!AreTagsEqual(_tags, _tagsOriginal, _tagComparer));
         }
      }

      /// <summary>
      /// Gets the original set of tags (keywords).
      /// </summary>
      public string[] TagsOriginal
      {
         get
         {
            return _tagsOriginal;
         }
      }

      /// <summary>
      /// Gets or set the title.
      /// </summary>
      public string Title
      {
         get
         {
            return _title;
         }
         set
         {
            if (_title != value)
            {
               _title = value;
               if (!_trackChanges)
               {
                  _titleOriginal = value;
               }
               else
               {
                  _changed = true;
               }
            }
         }
      }

      /// <summary>
      /// Gets whether or not the title has changed.
      /// </summary>
      public bool TitleChanged
      {
         get
         {
            return (_title != _titleOriginal);
         }
      }

      /// <summary>
      /// Gets the original title.
      /// </summary>
      public string TitleOriginal
      {
         get
         {
            return _titleOriginal;
         }
      }

      /// <summary>
      /// Gets or set the description.
      /// </summary>
      public string Description
      {
         get
         {
            return _description;
         }
         set
         {
            if (_description != value)
            {
               _description = value;
               if (!_trackChanges)
               {
                  _descriptionOriginal = value;
               }
               else
               {
                  _changed = true;
               }
            }
         }
      }

      /// <summary>
      /// Gets whether or not the description has changed.
      /// </summary>
      public bool DescriptionChanged
      {
         get
         {
            return (_description != _descriptionOriginal);
         }
      }

      /// <summary>
      /// Gets the original description.
      /// </summary>
      public string DescriptionOriginal
      {
         get
         {
            return _descriptionOriginal;
         }
      }

      /// <summary>
      /// Gets or set the author.
      /// </summary>
      public string Author
      {
         get
         {
            return _author;
         }
         set
         {
            if (_author != value)
            {
               _author = value;
               if (!_trackChanges)
               {
                  _authorOriginal = value;
               }
               else
               {
                  _changed = true;
               }
            }
         }
      }

      /// <summary>
      /// Gets whether or not the author has changed.
      /// </summary>
      public bool AuthorChanged
      {
         get
         {
            return (_author != _authorOriginal);
         }
      }

      /// <summary>
      /// Gets the original author.
      /// </summary>
      public string AuthorOriginal
      {
         get
         {
            return _authorOriginal;
         }
      }

      /// <summary>
      /// Gets or sets whether or not any property has changed.
      /// </summary>
      public bool Changed
      {
         get
         {
            return _changed;
         }
         protected set
         {
            _changed = true;
         }
      }

      /// <summary>
      /// Gets or sets whether or not changes should be tracked.
      /// </summary>
      /// <remarks>When this property is set, <see cref="Changed"/> is set to false
      /// until another property is set.</remarks>
      public bool TrackChanges
      {
         get
         {
            return _trackChanges;
         }
         set
         {
            _trackChanges = value;
            _changed = false;
         }
      }
      #endregion

      #region Public static methods
      /// <summary>
      /// Checks if two metadata containers contain the same tags.
      /// </summary>
      /// <param name="m1">The first metadata container.</param>
      /// <param name="m2">The second metadata container.</param>
      /// <param name="comparer">The string comparer to use.</param>
      /// <returns><c>True</c> if they contain the same tags, <c>false</c> otherwise.</returns>
      public static bool AreTagsEqual(PictureMetadata m1, PictureMetadata m2, StringComparer comparer)
      {
         return AreTagsEqual(m1.Tags, m2.Tags, comparer);
      }
      #endregion

      #region Private static methods
      /// <summary>
      /// Checks if the two arrays of tags contain the same tags.
      /// </summary>
      /// <param name="tags1">The first set of tags.</param>
      /// <param name="tags2">The second set of tags.</param>
      /// <param name="comparer">The string comparer to use.</param>
      /// <returns><c>True</c> if they contain the same tags, <c>false</c> otherwise.</returns>
      private static bool AreTagsEqual(string[] tags1, string[] tags2, IEqualityComparer<string> comparer)
      {
         if (tags1.Length != tags2.Length)
         {
            return false;
         }
         for (int i = 0; i < tags1.Length; i++)
         {
            if (!HasTag(tags2, tags1[i], comparer))
            {
               return false;
            }
         }

         return true;
      }

      /// <summary>
      /// Checks if <paramref name="tag"/> occurs in <paramref name="tags"/>.
      /// </summary>
      /// <param name="tags">The tags to check.</param>
      /// <param name="tag">The tag to find.</param>
      /// <param name="comparer">The string comparer to use.</param>
      /// <returns><c>True</c> if the tag was found, <c>false</c> otherwise.</returns>
      private static bool HasTag(string[] tags, string tag, IEqualityComparer<string> comparer)
      {
         for (int i = 0; i < tags.Length; i++)
         {
            if (comparer.Equals(tags[i], tag))
            {
               return true;
            }
         }
         return false;
      }
      #endregion

      #region Public static methods
      /// <summary>
      /// Determines if two metadata containers can be considered to contain
      /// the same metadata.
      /// </summary>
      /// <param name="m1">The first metadata container.</param>
      /// <param name="m2">The second metadata container.</param>
      /// <returns><c>True</c> if the metadata is the same, <c>false</c> otherwise.</returns>
      /// <remarks>If the two metadata containers have the same metadata, no synchronization
      /// is needed.</remarks>
      public static bool AreEqual(PictureMetadata m1, PictureMetadata m2)
      {
         return (m1.Title == m2.Title && m1.Description == m2.Description
           && m1.Author == m2.Author && m1.DateTaken == m2.DateTaken
           && m1.GpsLatitude == m2.GpsLatitude && m1.GpsLongitude == m2.GpsLongitude
           && AreTagsEqual(m1, m2, _tagComparer));
      }
      #endregion

      #region Protected methods
      /// <summary>
      /// Copies properties from another instance to the current instance.
      /// </summary>
      /// <param name="toCopy">The instance to copy properties from.</param>
      protected void CopyProperties(PictureMetadata toCopy)
      {
         this.Author = toCopy.Author;
         this.Title = toCopy.Title;
         this.Description = toCopy.Description;
         this.DateTaken = toCopy.DateTaken;
         this.LastUpdated = toCopy.LastUpdated;
         this.GpsLatitude = toCopy.GpsLatitude;
         this.GpsLongitude = toCopy.GpsLongitude;
         this.Tags = (string[])toCopy.Tags.Clone();
      }
      #endregion
   }
}
